<!DOCTYPE html>
<html lang="zh-cn">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width">
    <script type="application/javascript" src='https://kk-karl.github.io/booklog/js/theme-mode.js'></script>
    <link rel="stylesheet" href='https://kk-karl.github.io/booklog/css/frameworks.min.css' />
    <link rel="stylesheet" href='https://kk-karl.github.io/booklog/css/github.min.css' />
    <link rel="stylesheet" href='https://kk-karl.github.io/booklog/css/github-style.css' />
    <link rel="stylesheet" href='https://kk-karl.github.io/booklog/css/light.css' />
    <link rel="stylesheet" href='https://kk-karl.github.io/booklog/css/dark.css' />
    <link rel="stylesheet" href='https://kk-karl.github.io/booklog/css/syntax.css' />
    <title>RocketMQ学习 - mybooklog</title>
    
    <link rel="icon" type="image/x-icon" href='../../assets/favicon.ico'>
    
    <meta name="theme-color" content="#1e2327">

    
    <meta name="description"
  content="MQ有什么用 解耦 我觉得从某种角度来说，微服务促进了MQ的蓬勃发展，本来一个系统有N多个模块，所有模块都强耦合在一起，现在微服务了，一个模块就是一个系统，系统之间肯定需要交互，交互有三种常见的方法，一种是RPC，一种是HTTP，一种就是MQ了。
异步 原本一个业务分为N步，要一步一步处理，才能把最终的结果返回给用户，现在有了MQ，先把最关键的部分处理完毕，然后发送消息到MQ，直接返回给用户OK，至于后面的步骤在后台慢慢处理吧，真乃提升用户体验的神器。
削峰 某个接口的请求量突然飙升，势必会对应用服务器、数据库服务器造成很大的压力，现在有了MQ，来多少请求都不在怕的，后台慢慢处理呗。
RocketMQ简介 RocketMQ是用Java编写的，是阿里开源的消息中间件，吸收了Kafka很多优点。Kafka也是比较热门的消息中间件，不过Kafka是用Scala编写的，不利于Java程序员去阅读源码，也不利于Java程序员做一些定制化的开发。接触过Kafka的小伙伴都知道，要用好Kafka实属不易，相对来说，RocketMQ简单多了，而且RocketMQ有阿里加持，经历了N次双11的考验，比较适合国内互联网公司，所以国内使用RocketMQ的公司很多。
RocketMQ四大组件 图片来自https://gitee.com/mirrors/rocketmq/blob/master/docs/cn/architecture.md
可以看到RocketMQ主要有四个组件：
NameServer 无状态服务，注册中心，可集群部署，但是NameServer节点之间没有任何数据交互。 Broker会以定时把Topic路由信息上报给所有的NameServer。Producer、Consumer会随机选择一个NameServer定时Topic更新路由信息。 Topic路由信息在NameServer集群中采用最终一致性。 保证AP。 Broker RocketMQ的服务端，用于存储消息、分发消息。 Broker会定时把自身拥有的所有的Topic路由信息上报给NameServer。 Broker有两个角色：Master、Follower，Master承担读（消费消息）写（生产消息）操作，如果Master比较忙，或者不可用，Follower可以承担读操作。BrokerId=0，代表是Matser，BrokerId!=0，代表是Follower，需要注意的有两点： 其一，目前为止，BrokerId=1的Follower才可以承担读操作； 其二，只有较高版本的RocketMQ才支持当Master节点挂掉，Follower自动升级到Master。 Producer 生产者，每隔一定时间向NameServer发起Topic的路由信息查询。
Consumer 消费者，每隔一定时间向NameServer发起Topic的路由信息查询。
为什么注册中心不选用Zookeeper 其实，在低版本的RocketMQ中，确实是选用Zookeeper作为注册中心的，但是后面改成了现在的NameServer，猜想主要原因是：
RocketMQ已经是一个中间件了，不想再依赖其他中间件。 Zookeeper比较重，有很多功能RocketMQ是用不到的，不如写一个轻量级的注册中心。 Zookeeper是CP，一旦触发领导选举，那么注册中心就不可用了，而RocketMQ的注册中心，不需要强一致性，只要保证最终一致性。 RocketMQ消息领域模型 Message 传输的消息。 消息必须有Topic。 消息可以有多个Tag和多个Key，可以看做消息的附加属性。 Topic 一类消息的集合。 每个消息必须有一个Topic。 消息的第一级类型。 Tag 一个消息除了有Topic之外，还可以有Tag，用来细分同一个Topic下的不同种类的消息。 Tag不是必须的。 消息的第二级类型。 Group 分为ProducerGroup，ConsumerGroup，我们更多的是关注ConsumerGroup，ConsumerGroup包含多个Consumer。
在集群消费模式下，一个ConsumerGroup下的Consumer共同消费一个Topic，且每个Consumer会被分配到N个队列，但是一个队列只会被一个Consumer消费，不同的ConsumerGroup可以消费同一个Topic，一条消息会被订阅此Topic的所有ConsumerGroup消费。
Queue 一个Topic默认包含四个Queue。 在集群消费模式下，同一个ConsumerGroup中的Consumer可以消费多个Queue的消息，但是一个Queue只能被一个Consumer消费。 Queue中的消息是有序的。 分为读Queue和写Queue，一般来说，读Queue的数量和写Queue的数量是一致的，否则很容易出问题。 消费模式 消费模式有两种：Clustering（集群消费）和Broadcasting（广播消费）。
和其他MQ不同，其他MQ是在发送消息的时候，指定是集群消费还是广播消费，RocketMQ是在消费者端设置是集群消费还是广播消费。
Clustering（集群消费） 默认情况下是集群消费模式，该模式下，ConsumerGroup所有的Consumer共同消费一个Topic的消息，每个Consumer负责消费N个队列的消息（N也可能为1，甚至是0，没有分配到队列），但是一个队列只会被一个Consumer消费。如果某个Consumer挂掉，ConsumerGroup下的其他Consumer会接替挂掉的Consumer继续消费。
集群消费模式下，消费进度维护在Broker端，存储路径为${ROCKET_HOME}/store/config/ consumerOffset.json，如下图所示： 使用topicName@consumerGroupName为Key，消费进度为Value，Value的形式是queueId:offset ，说明如果有多个ConsumerGroup，每个ConsumerGroup的消费进度是不同的，需要分开来存储。
Broadcasting（广播消费） 广播消费消息会发给ConsumerGroup中所有的Consumer。
广播消费模式下，消费进度维护在Consumer端。
消费队列负载算法与重平衡机制 消费队列负载算法 我们知道了在集群消费模式下，ConsumerGroup下所有的Consumer共同消费一个Topic的消息，每个Consumer负责消费N个队列的消息，那么具体是如何分配的呢？这就涉及到消费队列负载算法了。
RocketMQ提供了众多的消费队列负载算法，其中最常用的是两种算法，即AllocateMessageQueueAveragely、AllocateMessageQueueAveragelyByCircle。下面我们来看下这两个算法的区别。
假设，现在一个Topic有16个队列，用q0~q15表示，有3个Consumer，用c0-c2表示。" />
<meta name="keywords"
  content='blog,java,life, google analytics' />
<meta name="robots" content="noodp" />
<link rel="canonical" href="https://kk-karl.github.io/booklog/post/myblg03/" />


<meta name="twitter:card" content="summary" />
<meta name="twitter:title" content="RocketMQ学习 - mybooklog" />
<meta name="twitter:description"
  content="MQ有什么用 解耦 我觉得从某种角度来说，微服务促进了MQ的蓬勃发展，本来一个系统有N多个模块，所有模块都强耦合在一起，现在微服务了，一个模块就是一个系统，系统之间肯定需要交互，交互有三种常见的方法，一种是RPC，一种是HTTP，一种就是MQ了。
异步 原本一个业务分为N步，要一步一步处理，才能把最终的结果返回给用户，现在有了MQ，先把最关键的部分处理完毕，然后发送消息到MQ，直接返回给用户OK，至于后面的步骤在后台慢慢处理吧，真乃提升用户体验的神器。
削峰 某个接口的请求量突然飙升，势必会对应用服务器、数据库服务器造成很大的压力，现在有了MQ，来多少请求都不在怕的，后台慢慢处理呗。
RocketMQ简介 RocketMQ是用Java编写的，是阿里开源的消息中间件，吸收了Kafka很多优点。Kafka也是比较热门的消息中间件，不过Kafka是用Scala编写的，不利于Java程序员去阅读源码，也不利于Java程序员做一些定制化的开发。接触过Kafka的小伙伴都知道，要用好Kafka实属不易，相对来说，RocketMQ简单多了，而且RocketMQ有阿里加持，经历了N次双11的考验，比较适合国内互联网公司，所以国内使用RocketMQ的公司很多。
RocketMQ四大组件 图片来自https://gitee.com/mirrors/rocketmq/blob/master/docs/cn/architecture.md
可以看到RocketMQ主要有四个组件：
NameServer 无状态服务，注册中心，可集群部署，但是NameServer节点之间没有任何数据交互。 Broker会以定时把Topic路由信息上报给所有的NameServer。Producer、Consumer会随机选择一个NameServer定时Topic更新路由信息。 Topic路由信息在NameServer集群中采用最终一致性。 保证AP。 Broker RocketMQ的服务端，用于存储消息、分发消息。 Broker会定时把自身拥有的所有的Topic路由信息上报给NameServer。 Broker有两个角色：Master、Follower，Master承担读（消费消息）写（生产消息）操作，如果Master比较忙，或者不可用，Follower可以承担读操作。BrokerId=0，代表是Matser，BrokerId!=0，代表是Follower，需要注意的有两点： 其一，目前为止，BrokerId=1的Follower才可以承担读操作； 其二，只有较高版本的RocketMQ才支持当Master节点挂掉，Follower自动升级到Master。 Producer 生产者，每隔一定时间向NameServer发起Topic的路由信息查询。
Consumer 消费者，每隔一定时间向NameServer发起Topic的路由信息查询。
为什么注册中心不选用Zookeeper 其实，在低版本的RocketMQ中，确实是选用Zookeeper作为注册中心的，但是后面改成了现在的NameServer，猜想主要原因是：
RocketMQ已经是一个中间件了，不想再依赖其他中间件。 Zookeeper比较重，有很多功能RocketMQ是用不到的，不如写一个轻量级的注册中心。 Zookeeper是CP，一旦触发领导选举，那么注册中心就不可用了，而RocketMQ的注册中心，不需要强一致性，只要保证最终一致性。 RocketMQ消息领域模型 Message 传输的消息。 消息必须有Topic。 消息可以有多个Tag和多个Key，可以看做消息的附加属性。 Topic 一类消息的集合。 每个消息必须有一个Topic。 消息的第一级类型。 Tag 一个消息除了有Topic之外，还可以有Tag，用来细分同一个Topic下的不同种类的消息。 Tag不是必须的。 消息的第二级类型。 Group 分为ProducerGroup，ConsumerGroup，我们更多的是关注ConsumerGroup，ConsumerGroup包含多个Consumer。
在集群消费模式下，一个ConsumerGroup下的Consumer共同消费一个Topic，且每个Consumer会被分配到N个队列，但是一个队列只会被一个Consumer消费，不同的ConsumerGroup可以消费同一个Topic，一条消息会被订阅此Topic的所有ConsumerGroup消费。
Queue 一个Topic默认包含四个Queue。 在集群消费模式下，同一个ConsumerGroup中的Consumer可以消费多个Queue的消息，但是一个Queue只能被一个Consumer消费。 Queue中的消息是有序的。 分为读Queue和写Queue，一般来说，读Queue的数量和写Queue的数量是一致的，否则很容易出问题。 消费模式 消费模式有两种：Clustering（集群消费）和Broadcasting（广播消费）。
和其他MQ不同，其他MQ是在发送消息的时候，指定是集群消费还是广播消费，RocketMQ是在消费者端设置是集群消费还是广播消费。
Clustering（集群消费） 默认情况下是集群消费模式，该模式下，ConsumerGroup所有的Consumer共同消费一个Topic的消息，每个Consumer负责消费N个队列的消息（N也可能为1，甚至是0，没有分配到队列），但是一个队列只会被一个Consumer消费。如果某个Consumer挂掉，ConsumerGroup下的其他Consumer会接替挂掉的Consumer继续消费。
集群消费模式下，消费进度维护在Broker端，存储路径为${ROCKET_HOME}/store/config/ consumerOffset.json，如下图所示： 使用topicName@consumerGroupName为Key，消费进度为Value，Value的形式是queueId:offset ，说明如果有多个ConsumerGroup，每个ConsumerGroup的消费进度是不同的，需要分开来存储。
Broadcasting（广播消费） 广播消费消息会发给ConsumerGroup中所有的Consumer。
广播消费模式下，消费进度维护在Consumer端。
消费队列负载算法与重平衡机制 消费队列负载算法 我们知道了在集群消费模式下，ConsumerGroup下所有的Consumer共同消费一个Topic的消息，每个Consumer负责消费N个队列的消息，那么具体是如何分配的呢？这就涉及到消费队列负载算法了。
RocketMQ提供了众多的消费队列负载算法，其中最常用的是两种算法，即AllocateMessageQueueAveragely、AllocateMessageQueueAveragelyByCircle。下面我们来看下这两个算法的区别。
假设，现在一个Topic有16个队列，用q0~q15表示，有3个Consumer，用c0-c2表示。" />
<meta name="twitter:site" content="https://kk-karl.github.io/booklog/" />
<meta name="twitter:creator" content="" />
<meta name="twitter:image"
  content="https://kk-karl.github.io/booklog/">


<meta property="og:type" content="article" />
<meta property="og:title" content="RocketMQ学习 - mybooklog">
<meta property="og:description"
  content="MQ有什么用 解耦 我觉得从某种角度来说，微服务促进了MQ的蓬勃发展，本来一个系统有N多个模块，所有模块都强耦合在一起，现在微服务了，一个模块就是一个系统，系统之间肯定需要交互，交互有三种常见的方法，一种是RPC，一种是HTTP，一种就是MQ了。
异步 原本一个业务分为N步，要一步一步处理，才能把最终的结果返回给用户，现在有了MQ，先把最关键的部分处理完毕，然后发送消息到MQ，直接返回给用户OK，至于后面的步骤在后台慢慢处理吧，真乃提升用户体验的神器。
削峰 某个接口的请求量突然飙升，势必会对应用服务器、数据库服务器造成很大的压力，现在有了MQ，来多少请求都不在怕的，后台慢慢处理呗。
RocketMQ简介 RocketMQ是用Java编写的，是阿里开源的消息中间件，吸收了Kafka很多优点。Kafka也是比较热门的消息中间件，不过Kafka是用Scala编写的，不利于Java程序员去阅读源码，也不利于Java程序员做一些定制化的开发。接触过Kafka的小伙伴都知道，要用好Kafka实属不易，相对来说，RocketMQ简单多了，而且RocketMQ有阿里加持，经历了N次双11的考验，比较适合国内互联网公司，所以国内使用RocketMQ的公司很多。
RocketMQ四大组件 图片来自https://gitee.com/mirrors/rocketmq/blob/master/docs/cn/architecture.md
可以看到RocketMQ主要有四个组件：
NameServer 无状态服务，注册中心，可集群部署，但是NameServer节点之间没有任何数据交互。 Broker会以定时把Topic路由信息上报给所有的NameServer。Producer、Consumer会随机选择一个NameServer定时Topic更新路由信息。 Topic路由信息在NameServer集群中采用最终一致性。 保证AP。 Broker RocketMQ的服务端，用于存储消息、分发消息。 Broker会定时把自身拥有的所有的Topic路由信息上报给NameServer。 Broker有两个角色：Master、Follower，Master承担读（消费消息）写（生产消息）操作，如果Master比较忙，或者不可用，Follower可以承担读操作。BrokerId=0，代表是Matser，BrokerId!=0，代表是Follower，需要注意的有两点： 其一，目前为止，BrokerId=1的Follower才可以承担读操作； 其二，只有较高版本的RocketMQ才支持当Master节点挂掉，Follower自动升级到Master。 Producer 生产者，每隔一定时间向NameServer发起Topic的路由信息查询。
Consumer 消费者，每隔一定时间向NameServer发起Topic的路由信息查询。
为什么注册中心不选用Zookeeper 其实，在低版本的RocketMQ中，确实是选用Zookeeper作为注册中心的，但是后面改成了现在的NameServer，猜想主要原因是：
RocketMQ已经是一个中间件了，不想再依赖其他中间件。 Zookeeper比较重，有很多功能RocketMQ是用不到的，不如写一个轻量级的注册中心。 Zookeeper是CP，一旦触发领导选举，那么注册中心就不可用了，而RocketMQ的注册中心，不需要强一致性，只要保证最终一致性。 RocketMQ消息领域模型 Message 传输的消息。 消息必须有Topic。 消息可以有多个Tag和多个Key，可以看做消息的附加属性。 Topic 一类消息的集合。 每个消息必须有一个Topic。 消息的第一级类型。 Tag 一个消息除了有Topic之外，还可以有Tag，用来细分同一个Topic下的不同种类的消息。 Tag不是必须的。 消息的第二级类型。 Group 分为ProducerGroup，ConsumerGroup，我们更多的是关注ConsumerGroup，ConsumerGroup包含多个Consumer。
在集群消费模式下，一个ConsumerGroup下的Consumer共同消费一个Topic，且每个Consumer会被分配到N个队列，但是一个队列只会被一个Consumer消费，不同的ConsumerGroup可以消费同一个Topic，一条消息会被订阅此Topic的所有ConsumerGroup消费。
Queue 一个Topic默认包含四个Queue。 在集群消费模式下，同一个ConsumerGroup中的Consumer可以消费多个Queue的消息，但是一个Queue只能被一个Consumer消费。 Queue中的消息是有序的。 分为读Queue和写Queue，一般来说，读Queue的数量和写Queue的数量是一致的，否则很容易出问题。 消费模式 消费模式有两种：Clustering（集群消费）和Broadcasting（广播消费）。
和其他MQ不同，其他MQ是在发送消息的时候，指定是集群消费还是广播消费，RocketMQ是在消费者端设置是集群消费还是广播消费。
Clustering（集群消费） 默认情况下是集群消费模式，该模式下，ConsumerGroup所有的Consumer共同消费一个Topic的消息，每个Consumer负责消费N个队列的消息（N也可能为1，甚至是0，没有分配到队列），但是一个队列只会被一个Consumer消费。如果某个Consumer挂掉，ConsumerGroup下的其他Consumer会接替挂掉的Consumer继续消费。
集群消费模式下，消费进度维护在Broker端，存储路径为${ROCKET_HOME}/store/config/ consumerOffset.json，如下图所示： 使用topicName@consumerGroupName为Key，消费进度为Value，Value的形式是queueId:offset ，说明如果有多个ConsumerGroup，每个ConsumerGroup的消费进度是不同的，需要分开来存储。
Broadcasting（广播消费） 广播消费消息会发给ConsumerGroup中所有的Consumer。
广播消费模式下，消费进度维护在Consumer端。
消费队列负载算法与重平衡机制 消费队列负载算法 我们知道了在集群消费模式下，ConsumerGroup下所有的Consumer共同消费一个Topic的消息，每个Consumer负责消费N个队列的消息，那么具体是如何分配的呢？这就涉及到消费队列负载算法了。
RocketMQ提供了众多的消费队列负载算法，其中最常用的是两种算法，即AllocateMessageQueueAveragely、AllocateMessageQueueAveragelyByCircle。下面我们来看下这两个算法的区别。
假设，现在一个Topic有16个队列，用q0~q15表示，有3个Consumer，用c0-c2表示。" />
<meta property="og:url" content="https://kk-karl.github.io/booklog/post/myblg03/" />
<meta property="og:site_name" content="RocketMQ学习" />
<meta property="og:image"
  content="https://kk-karl.github.io/booklog/">
<meta property="og:image:width" content="2048">
<meta property="og:image:height" content="1024">

<meta property="article:published_time" content="2024-04-11 13:47:41 &#43;0800 CST" />











</head>

<body>
  <div style="position: relative">
  <header class="Header js-details-container Details px-3 px-md-4 px-lg-5 flex-wrap flex-md-nowrap open Details--on">
    <div class="Header-item mobile-none" style="margin-top: -4px; margin-bottom: -4px;">
      <a class="Header-link" href="https://kk-karl.github.io/booklog/">
        <img class="octicon" height="32" width="32" src="../../images/github-mark-white.png">
      </a>
    </div>
    <div class="Header-item d-md-none">
      <button class="Header-link btn-link js-details-target" type="button"
        onclick="document.querySelector('#header-search').style.display = document.querySelector('#header-search').style.display == 'none'? 'block': 'none'">
        <img height="24" class="octicon octicon-three-bars" width="24" src="../../images/github-mark-white.png">
      </button>
    </div>
    <div style="display: none;" id="header-search"
      class="Header-item Header-item--full flex-column flex-md-row width-full flex-order-2 flex-md-order-none mr-0 mr-md-3 mt-3 mt-md-0 Details-content--hidden-not-important d-md-flex">
      <div
        class="Header-search header-search flex-auto js-site-search position-relative flex-self-stretch flex-md-self-auto mb-3 mb-md-0 mr-0 mr-md-3 scoped-search site-scoped-search js-jump-to">
        <div class="position-relative">
          <form target="_blank" action="https://www.google.com/search" accept-charset="UTF-8" method="get"
            autocomplete="off">
            <label
              class="Header-search-label form-control input-sm header-search-wrapper p-0 js-chromeless-input-container header-search-wrapper-jump-to position-relative d-flex flex-justify-between flex-items-center">
              <input type="text"
                class="Header-search-input form-control input-sm header-search-input jump-to-field js-jump-to-field js-site-search-focus js-site-search-field is-clearable"
                name="q" value="" placeholder="Search" autocomplete="off">
              <input type="hidden" name="q" value="site:https://kk-karl.github.io/booklog/">
            </label>
          </form>
        </div>
      </div>
    </div>

    <div class="Header-item Header-item--full flex-justify-center d-md-none position-relative">
      <a class="Header-link " href="https://kk-karl.github.io/booklog/">
        <img class="octicon octicon-mark-github v-align-middle" height="32" width="32" src="../../images/github-mark-white.png">
      </a>
    </div>
    <div class="Header-item" style="margin-right: 0;">
      <a href="javascript:void(0)" class="Header-link no-select" onclick="switchTheme()">
        <svg style="fill: var(--color-profile-color-modes-toggle-moon);" class="no-select" viewBox="0 0 16 16"
          version="1.1" width="16" height="16">
          <path fill-rule="evenodd" clip-rule="evenodd"
            d="M4.52208 7.71754C7.5782 7.71754 10.0557 5.24006 10.0557 2.18394C10.0557 1.93498 10.0392 1.68986 10.0074 1.44961C9.95801 1.07727 10.3495 0.771159 10.6474 0.99992C12.1153 2.12716 13.0615 3.89999 13.0615 5.89383C13.0615 9.29958 10.3006 12.0605 6.89485 12.0605C3.95334 12.0605 1.49286 10.001 0.876728 7.24527C0.794841 6.87902 1.23668 6.65289 1.55321 6.85451C2.41106 7.40095 3.4296 7.71754 4.52208 7.71754Z">
          </path>
        </svg>
      </a>
    </div>
  </header>
</div>

  
<div>
  <main>
    <div class="gisthead pagehead bg-gray-light pb-0 pt-3 mb-4">
      <div class="px-0">
        <div class="mb-3 d-flex px-3 px-md-3 px-lg-5">
          <div class="flex-auto min-width-0 width-fit mr-3">
            <div class="d-flex">
              <div class="d-none d-md-block">
                <a class="avatar mr-2 flex-shrink-0" href="https://kk-karl.github.io/booklog/">
                  <img class=" avatar-user"
                    src="../../images/avatar.png"
                    width="32" height="32"></a>
              </div>
              <div class="d-flex flex-column">
                <h1 class="break-word f3 text-normal mb-md-0 mb-1">
                  <span class="author">
                    <a href="https://kk-karl.github.io/booklog/">kk-karl</a>
                  </span>
                  <span class="path-divider">/</span>
                  <strong class="css-truncate css-truncate-target mr-1" style="max-width: 410px">
                    <a href="https://kk-karl.github.io/booklog/post/myblg03/">RocketMQ学习</a>
                  </strong>
                </h1>
                <div class="note m-0">
                  Created <relative-time datetime="Thu, 11 Apr 2024 13:47:41 &#43;0800"
                    class="no-wrap">
                    Thu, 11 Apr 2024 13:47:41 &#43;0800</relative-time>

                  
                  <span class="file-info-divider"></span>
                  Modified <relative-time datetime="Tue, 23 Apr 2024 22:27:41 &#43;0800"
                    class="no-wrap">
                    Tue, 23 Apr 2024 22:27:41 &#43;0800</relative-time>
                  
                </div>
              </div>
            </div>
          </div>
        </div>
      </div>
    </div>

    <div class="container-lg px-3 new-discussion-timeline">
      <div class="repository-content gist-content">
        <div>
          <div class="js-gist-file-update-container js-task-list-container file-box">
            <div id="file-pytest" class="file my-2">
              <div id="post-header" class="file-header d-flex flex-md-items-center flex-items-start sticky-header" style="z-index: 2">
                <div class="file-info d-flex flex-md-items-center flex-items-start flex-order-1 flex-auto">
                  <div class="text-mono f6 flex-auto pr-3 flex-order-2 flex-md-order-1 mt-2 mt-md-0">
                    
                    <summary id="toc-toggle" onclick="clickToc()" class="btn btn-octicon m-0 mr-2 p-2">
                      <svg aria-hidden="true" viewBox="0 0 16 16" height="16" width="16" class="octicon octicon-list-unordered">
                        <path fill-rule="evenodd" d="M2 4a1 1 0 100-2 1 1 0 000 2zm3.75-1.5a.75.75 0 000 1.5h8.5a.75.75 0 000-1.5h-8.5zm0 5a.75.75 0 000 1.5h8.5a.75.75 0 000-1.5h-8.5zm0 5a.75.75 0 000 1.5h8.5a.75.75 0 000-1.5h-8.5zM3 8a1 1 0 11-2 0 1 1 0 012 0zm-1 6a1 1 0 100-2 1 1 0 000 2z"></path>
                      </svg>
                    </summary>
                    <details-menu class="SelectMenu" id="toc-details" style="display: none;">
                      <div class="SelectMenu-modal rounded-3 mt-1" style="max-height: 340px;">
                        <div class="SelectMenu-list SelectMenu-list--borderless p-2" style="overscroll-behavior: contain;" id="toc-list">
                        </div>
                      </div>
                    </details-menu>
                      6650 Words
                     <span class="file-info-divider"></span>
                                        1 min

                  </div>
                  <div class="file-actions flex-order-2 pt-0">
                    
                    
                    
                    <a class="muted-link mr-3" href="../../tags/mq" >
                      <svg class="octicon octicon-tag" viewBox="0 0 16 16" version="1.1" width="16" height="16">
                        <path fill-rule="evenodd"
                          d="M2.5 7.775V2.75a.25.25 0 01.25-.25h5.025a.25.25 0 01.177.073l6.25 6.25a.25.25 0 010 .354l-5.025 5.025a.25.25 0 01-.354 0l-6.25-6.25a.25.25 0 01-.073-.177zm-1.5 0V2.75C1 1.784 1.784 1 2.75 1h5.025c.464 0 .91.184 1.238.513l6.25 6.25a1.75 1.75 0 010 2.474l-5.026 5.026a1.75 1.75 0 01-2.474 0l-6.25-6.25A1.75 1.75 0 011 7.775zM6 5a1 1 0 100 2 1 1 0 000-2z">
                        </path>
                      </svg>
                      MQ
                    </a>
                    
                    
                    <a class="muted-link mr-3" href="../../tags/java" >
                      <svg class="octicon octicon-tag" viewBox="0 0 16 16" version="1.1" width="16" height="16">
                        <path fill-rule="evenodd"
                          d="M2.5 7.775V2.75a.25.25 0 01.25-.25h5.025a.25.25 0 01.177.073l6.25 6.25a.25.25 0 010 .354l-5.025 5.025a.25.25 0 01-.354 0l-6.25-6.25a.25.25 0 01-.073-.177zm-1.5 0V2.75C1 1.784 1.784 1 2.75 1h5.025c.464 0 .91.184 1.238.513l6.25 6.25a1.75 1.75 0 010 2.474l-5.026 5.026a1.75 1.75 0 01-2.474 0l-6.25-6.25A1.75 1.75 0 011 7.775zM6 5a1 1 0 100 2 1 1 0 000-2z">
                        </path>
                      </svg>
                      java
                    </a>
                    
                    
                  </div>
                </div>
              </div>


              <div class="Box-body px-5 pb-5" style="z-index: 1">
                <article class="markdown-body entry-content container-lg"><h3 id="mq有什么用">MQ有什么用</h3>
<h4 id="解耦">解耦</h4>
<p>我觉得从某种角度来说，微服务促进了MQ的蓬勃发展，本来一个系统有N多个模块，所有模块都强耦合在一起，现在微服务了，一个模块就是一个系统，系统之间肯定需要交互，交互有三种常见的方法，一种是RPC，一种是HTTP，一种就是MQ了。</p>
<h4 id="异步">异步</h4>
<p>原本一个业务分为N步，要一步一步处理，才能把最终的结果返回给用户，现在有了MQ，先把最关键的部分处理完毕，然后发送消息到MQ，直接返回给用户OK，至于后面的步骤在后台慢慢处理吧，真乃提升用户体验的神器。</p>
<h4 id="削峰">削峰</h4>
<p>某个接口的请求量突然飙升，势必会对应用服务器、数据库服务器造成很大的压力，现在有了MQ，来多少请求都不在怕的，后台慢慢处理呗。</p>
<h3 id="rocketmq简介">RocketMQ简介</h3>
<p>RocketMQ是用Java编写的，是阿里开源的消息中间件，吸收了Kafka很多优点。Kafka也是比较热门的消息中间件，不过Kafka是用Scala编写的，不利于Java程序员去阅读源码，也不利于Java程序员做一些定制化的开发。接触过Kafka的小伙伴都知道，要用好Kafka实属不易，相对来说，RocketMQ简单多了，而且RocketMQ有阿里加持，经历了N次双11的考验，比较适合国内互联网公司，所以国内使用RocketMQ的公司很多。</p>
<h3 id="rocketmq四大组件">RocketMQ四大组件</h3>
<p><img src="https://upload-images.jianshu.io/upload_images/15100432-bfd337abee0cb241.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240" alt="image.png">
图片来自https://gitee.com/mirrors/rocketmq/blob/master/docs/cn/architecture.md</p>
<p>可以看到RocketMQ主要有四个组件：</p>
<h4 id="nameserver">NameServer</h4>
<ul>
<li>无状态服务，注册中心，可集群部署，但是NameServer节点之间没有任何数据交互。</li>
<li>Broker会以定时把Topic路由信息上报给所有的NameServer。Producer、Consumer会随机选择一个NameServer定时Topic更新路由信息。</li>
<li>Topic路由信息在NameServer集群中采用最终一致性。</li>
<li>保证AP。</li>
</ul>
<h4 id="broker">Broker</h4>
<ul>
<li>RocketMQ的服务端，用于存储消息、分发消息。</li>
<li>Broker会定时把自身拥有的所有的Topic路由信息上报给NameServer。</li>
<li>Broker有两个角色：Master、Follower，Master承担读（消费消息）写（生产消息）操作，如果Master比较忙，或者不可用，Follower可以承担读操作。BrokerId=0，代表是Matser，BrokerId!=0，代表是Follower，需要注意的有两点：
其一，目前为止，BrokerId=1的Follower才可以承担读操作；
其二，只有较高版本的RocketMQ才支持当Master节点挂掉，Follower自动升级到Master。</li>
</ul>
<h4 id="producer">Producer</h4>
<p>生产者，每隔一定时间向NameServer发起Topic的路由信息查询。</p>
<h4 id="consumer">Consumer</h4>
<p>消费者，每隔一定时间向NameServer发起Topic的路由信息查询。</p>
<h3 id="为什么注册中心不选用zookeeper">为什么注册中心不选用Zookeeper</h3>
<p>其实，在低版本的RocketMQ中，确实是选用Zookeeper作为注册中心的，但是后面改成了现在的NameServer，猜想主要原因是：</p>
<ul>
<li>RocketMQ已经是一个中间件了，不想再依赖其他中间件。</li>
<li>Zookeeper比较重，有很多功能RocketMQ是用不到的，不如写一个轻量级的注册中心。</li>
<li>Zookeeper是CP，一旦触发领导选举，那么注册中心就不可用了，而RocketMQ的注册中心，不需要强一致性，只要保证最终一致性。</li>
</ul>
<h3 id="rocketmq消息领域模型">RocketMQ消息领域模型</h3>
<h4 id="message">Message</h4>
<ul>
<li>传输的消息。</li>
<li>消息必须有Topic。</li>
<li>消息可以有多个Tag和多个Key，可以看做消息的附加属性。</li>
</ul>
<h4 id="topic">Topic</h4>
<ul>
<li>一类消息的集合。</li>
<li>每个消息必须有一个Topic。</li>
<li>消息的第一级类型。</li>
</ul>
<h4 id="tag">Tag</h4>
<ul>
<li>一个消息除了有Topic之外，还可以有Tag，用来细分同一个Topic下的不同种类的消息。</li>
<li>Tag不是必须的。</li>
<li>消息的第二级类型。</li>
</ul>
<h4 id="group">Group</h4>
<p>分为ProducerGroup，ConsumerGroup，我们更多的是关注ConsumerGroup，ConsumerGroup包含多个Consumer。</p>
<p>在集群消费模式下，一个ConsumerGroup下的Consumer共同消费一个Topic，且每个Consumer会被分配到N个队列，但是一个队列只会被一个Consumer消费，不同的ConsumerGroup可以消费同一个Topic，一条消息会被订阅此Topic的所有ConsumerGroup消费。</p>
<h4 id="queue">Queue</h4>
<ul>
<li>一个Topic默认包含四个Queue。</li>
<li>在集群消费模式下，同一个ConsumerGroup中的Consumer可以消费多个Queue的消息，但是一个Queue只能被一个Consumer消费。</li>
<li>Queue中的消息是有序的。</li>
<li>分为读Queue和写Queue，一般来说，读Queue的数量和写Queue的数量是一致的，否则很容易出问题。</li>
</ul>
<h3 id="消费模式">消费模式</h3>
<p>消费模式有两种：Clustering（集群消费）和Broadcasting（广播消费）。</p>
<p>和其他MQ不同，其他MQ是在发送消息的时候，指定是集群消费还是广播消费，RocketMQ是在消费者端设置是集群消费还是广播消费。</p>
<h4 id="clustering集群消费">Clustering（集群消费）</h4>
<p>默认情况下是集群消费模式，该模式下，ConsumerGroup所有的Consumer共同消费一个Topic的消息，每个Consumer负责消费N个队列的消息（N也可能为1，甚至是0，没有分配到队列），但是一个队列只会被一个Consumer消费。如果某个Consumer挂掉，ConsumerGroup下的其他Consumer会接替挂掉的Consumer继续消费。</p>
<p>集群消费模式下，消费进度维护在Broker端，存储路径为<code>${ROCKET_HOME}/store/config/ consumerOffset.json</code>，如下图所示：
<img src="https://upload-images.jianshu.io/upload_images/15100432-6cf2704757b4ca0a.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240" alt="image.png">
使用<code>topicName@consumerGroupName</code>为Key，消费进度为Value，Value的形式是<code>queueId:offset</code> ，说明如果有多个ConsumerGroup，每个ConsumerGroup的消费进度是不同的，需要分开来存储。</p>
<h4 id="broadcasting广播消费">Broadcasting（广播消费）</h4>
<p>广播消费消息会发给ConsumerGroup中所有的Consumer。</p>
<p>广播消费模式下，消费进度维护在Consumer端。</p>
<h3 id="消费队列负载算法与重平衡机制">消费队列负载算法与重平衡机制</h3>
<h4 id="消费队列负载算法">消费队列负载算法</h4>
<p>我们知道了在集群消费模式下，ConsumerGroup下所有的Consumer共同消费一个Topic的消息，每个Consumer负责消费N个队列的消息，那么具体是如何分配的呢？这就涉及到消费队列负载算法了。</p>
<p>RocketMQ提供了众多的消费队列负载算法，其中最常用的是两种算法，即AllocateMessageQueueAveragely、AllocateMessageQueueAveragelyByCircle。下面我们来看下这两个算法的区别。</p>
<p>假设，现在一个Topic有16个队列，用q0~q15表示，有3个Consumer，用c0-c2表示。</p>
<p>用AllocateMessageQueueAveragely消费队列负载算法的结果如下：</p>
<ul>
<li>c0：q0 q1 q2 q3 q4 q5</li>
<li>c1：q6 q7 q8 q9 q10</li>
<li>c2：q11 q12 q13 q14 q15</li>
</ul>
<p>用AllocateMessageQueueAveragelyByCircle消费队列负载算法的结果如下：</p>
<ul>
<li>c0：q0 q3 q6 q9 q12 q15</li>
<li>c1：q1 q4 q7 q10 q13</li>
<li>c2：q2 q5 q8 q11 q14</li>
</ul>
<p>ConsumerGroup下所有的Consumer共同消费一个Topic的消息，每个Consumer负责消费N个队列的消息，但是一个队列不能同时被N个Consumer消费，这意味着什么？</p>
<p>聪明的你一定可以想到，如果一个Topic只有4个队列，而有5个Consumer，那么有一个Consumer将不能分配到任何队列，所以在RocketMQ中，Topic下队列的个数直接决定了Consumer的最大个数，也就说明，不能光靠增加Consumer来提高消费速度。</p>
<h4 id="重平衡">重平衡</h4>
<p>虽然建议在创建Topic的时候，就应该充分考虑队列的个数，但是实际情况往往是不尽人意的，哪怕队列数没有发生改变，Consumer的数量也一定会发生改变，比如Consumer的上下线，比如某个Consumer挂了，比如新增了Consumer。队列的扩容、缩容，Consumer的扩容、缩容都会导致重平衡，也就是为Consumer重新分配消费的队列。</p>
<p>在RocketMQ中，Consumer会定时查询Topic的队列的个数，Consumer的个数，如果发生了改变，就会触发重平衡。</p>
<p>重平衡是RocketMQ内部实现的，程序员无需关心。</p>
<h3 id="pull-or-push">Pull OR Push？</h3>
<p>一般来说，MQ有两种方法获取消息：</p>
<ul>
<li>Pull：Consumer主动拉取消息，好处是Consumer可以控制拉取消息的频率，条数，Consumer知道自身的消费能力，所以在Consumer端不容易造成消息堆积，但是实时性不是太好，效率相对较低。</li>
<li>Push：Broker主动发送消息，好处是实时性、效率比较高，但是Broker无法知道Consumer端的消费能力，如果发给Consumer的消息过多，会造成Consumer端的消息堆积；如果发给Consumer的数据太少，又会造成Consumer端的空闲。</li>
</ul>
<p>不管是Pull，还是Push，Consumer总会与Broker产生交互，交互的方式一般有短连接、长连接、轮询三种方式。</p>
<p>看起来，RocketMQ支持既支持Pull，也支持Push，但是实际上Push也是用Pull实现的，那么Consumer是怎么与Broker产生交互的呢？</p>
<p>这就是RocketMQ设计的巧妙的地方了，既不是短连接，也不是长连接，也不是轮询，而是采用的长轮询。</p>
<h4 id="长轮询">长轮询</h4>
<p>Consumer发起拉取消息的请求，分为两种情况：</p>
<ul>
<li>有消息：Consumer拿到消息后，连接断开。</li>
<li>没有消息：Broker Hold（保持）住连接一定时间，每隔5秒，检查下是否有消息，如果有消息，给Consumer，连接断开。</li>
</ul>
<h3 id="事务消息">事务消息</h3>
<p>RocketMQ支持事务消息，Producer把事务消息发送给Broker后，Broker会把消息存储在系统Topic：<code>RMQ_SYS_TRANS_HALF_TOPIC</code>，这样Consumer就无法消费到这条消息了。</p>
<p>Broker会有一个定时任务，消费<code>RMQ_SYS_TRANS_HALF_TOPIC</code>的消息，向Producer发起回查，回查的状态有三种：提交、回滚、未知。</p>
<ul>
<li>如果回查的状态是提交，回滚，会触发消息的提交和回滚；</li>
<li>如果是未知，会等待下一次回查，RocketMQ可以设置一条消息的回查间隔与回查次数，超过一定的回查次数，消息会自动回滚。</li>
</ul>
<h3 id="延迟消息">延迟消息</h3>
<p>延迟消息是指息发到Broker后，不能立刻被Consumer消费，需要等待一定的时间才可以被消费到，RocketMQ只支持特定的延迟时间：<code>1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h</code>。</p>
<h3 id="消费形式">消费形式</h3>
<p>RocketMQ支持两种消费形式：并发消费、顺序消费。
如果是顺序消费，需要保证排序的消息在同一个队列。如何选择队列发送呢，RocketMQ发送消息的方法有好几个重载，其中有一个重载方法支持队列的选择。</p>
<h3 id="同步刷盘异步刷盘">同步刷盘、异步刷盘</h3>
<p>Producer把消息发送到Broker中，Broker是需要把消息持久化的，RocketMQ支持两种持久化策略：</p>
<ul>
<li>同步刷盘：Broker把消息持久后才返回ACK给Producer，好处是消息可靠性高，但是效率较慢。</li>
<li>异步刷盘：Broker把消息写入到PageCache中，就返回ACK给Producer。好处是效率极高，但是如果服务器挂了，消息可能会丢失，如果只是RocketMQ服务挂了，不会造成消息丢失。</li>
</ul>
<h3 id="同步复制异步复制">同步复制、异步复制</h3>
<p>为了MQ的可靠性、可用性，在生产环境，一般会部署Follower节点，Follower节点会复制Master的数据，RocketMQ支持两种持复制策略：</p>
<ul>
<li>同步复制：Master、Follower都把消息写入成功，才返回ACK给Producer，可靠性较高，但是效率较慢。</li>
<li>异步复制：只要Master写入成功，就返回ACK给Producer，效率较高，但是可能会丢失消息。</li>
</ul>
<p>&ldquo;写入&quot;是写入PageCache，还是写入硬盘，要看Follower Broker的配置。</p>
<h3 id="再谈谈producer">再谈谈Producer</h3>
<p>RocketMQ提供了三种发送消息的方法：</p>
<ul>
<li>oneway：fire and forget，单向消息，指消息发送出去后，就不管了，这个方法是没有返回值的。</li>
<li>同步：消息发送出去后，同步等待Broker的响应。</li>
<li>异步：消息发送出去后，立即返回，收到Broker的响应后，会执行函调方法。</li>
</ul>
<p>在实际开发中，一般选用同步方法，如果要提高RocketMQ的性能，一般都是修改Broker端的参数，特别是刷盘策略和复制策略。</p>
<h4 id="发送消息重试">发送消息重试</h4>
<p>消息发送时，如果使用了MessageQueueSelector，那消息发送的重试机制将会失效。</p>
<p>发送消息响应可能为以下四种：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-cpp" data-lang="cpp"><span class="line"><span class="cl"><span class="k">public</span> <span class="k">enum</span> <span class="nc">SendStatus</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="n">SEND_OK</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">FLUSH_DISK_TIMEOUT</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">FLUSH_SLAVE_TIMEOUT</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">SLAVE_NOT_AVAILABLE</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>除了第一种，其他情况都是有问题的，为了保证消息不丢失，需要设置Producer参数：<code>RetryAnotherBrokerWhenNotStoreOK</code>为true。</p>
<h5 id="故障规避机制">故障规避机制</h5>
<p>如果消息发送失败了，重试的时候，还是发送给这个Broker，那么大概率发送还是失败的，RocketMQ设计精巧之处在于，重试的时候，会自动避开这个Broker，而选择其他Broker，但是目前为止，异步发送没有那么智能，只会在一个Broker上重试，所以强烈建议选择同步发送方式。</p>
<p>RocketMQ提供了两种故障规避机制。用参数<code>SendLatencyFaultEnable</code>来控制。</p>
<ul>
<li>false：默认值，只有在重试的时候，才会启用故障规避机制，比如发送消息给BrokerA失败了，重试的时候，会选择BrokerB，但是下次发送消息，还是会选择发送给BrokerA。</li>
<li>true：开启延迟退避机制，一旦消息发送给BrokerA失败，就会悲观的认为在一段时间内，BrokerA不可用，在将来的一段时间内，不会再向BrokerA发送消息。</li>
</ul>
<p>延迟退避机制看起来很好用，但是一般来说Broker端繁忙，导致Broker不可用或者网络不可用只是一瞬间的事情，马上就可以恢复，如果开启了延迟退避机制，本来可用的Broker在一段时间内却被规避了，其他Broker更加繁忙，那可能情况更糟糕。</p>
<h3 id="再谈谈consumer">再谈谈Consumer</h3>
<h4 id="consumer线程注意事项">Consumer线程注意事项</h4>
<p>Consumer有两个参数，可以消费的并行度，即<code>ConsumeThreadMin</code>、<code>ConsumeThreadMax</code>，看起来给人的感觉是，如果Consumer端堆积消息比较少，消费线程数为<code>ConsumeThreadMin</code>；如果Consumer端堆积消息比较多，就自动开启新的线程来消费，直到消费线程数为<code>ConsumeThreadMax</code>。但是并不是这样，Consumer内部持有一个线程池，选用的是无界队列，也就是<code>ConsumeThreadMax</code>参数是无效的，所以在实际开发中，<code>ConsumeThreadMin</code>、<code>ConsumeThreadMax</code>往往设置成一样。</p>
<h4 id="consumefromwhere">ConsumeFromWhere</h4>
<p>如果查询不到消费进度的时候，Consumer从哪里开始消费，RocketMQ支持从最新消息、最早消息、指定时间戳这三种方式进行消费。</p>
<h4 id="消费消息重试">消费消息重试</h4>
<p>RocketMQ会为每个ConsumerGroup都设置一个Topic名称为<code>%RETRY%+consumerGroup</code>的重试队列，用来保存需要给ConsumerGroup重试的消息，但是重试需要一定的延时时间，RocketMQ对于重试消息的处理是先保存至Topic名称为<code>SCHEDULE_TOPIC_XXXX</code>的延迟队列中，后台定时任务按照对应的时间进行Delay后重新保存至<code>%RETRY%+consumerGroup</code>的重试队列中。</p>
<h3 id="消息堆积消费能力不够怎么办">消息堆积、消费能力不够，怎么办</h3>
<ul>
<li>提高消费进度，这是最好的办法。</li>
<li>增加队列，增加Consumer。</li>
<li>原先的Consumer作为搬砖工，根据一定的规则把消息“搬”到多个新的Topic，再开几个ConsumerGroup去消费不同的Topic。</li>
<li>新开一个ConsumerGroup去消费，也就是两个ConsumerGroup同时消费一个Topic，但是需要注意offset的判断，比如一个ConsumerGroup消费offset为奇数的消息，一个ConsumerGroup消费offset为偶数的消息</li>
</ul>
</article>
              </div>
            </div>
          </div>
        </div>
      </div>
    </div>
  </main>
</div>
<script type="application/javascript" src='https://kk-karl.github.io/booklog/js/toc.js'></script>
<link rel="stylesheet" href='https://kk-karl.github.io/booklog/css/toc.css' />


<div id="gitalk-container" class="gitalk-container"></div>
<link rel="stylesheet" href='https://kk-karl.github.io/booklog/css/gitalk.css'>
<script src='https://kk-karl.github.io/booklog/js/gitalk.min.js'></script>
<script>
  const gitalk = new Gitalk({
    clientID: 'Your client ID',
    clientSecret: 'Your client secret',
    repo: 'kk-karl.gitee.io',
    owner: 'kk-karl',
    admin: ['kk-karl'],
    id: eval("location.pathname"), 
    distractionFreeMode: false 
  });
  (function() {
    gitalk.render('gitalk-container');
  })();
</script>


  <div class="footer container-xl width-full p-responsive">
  <div
    class="position-relative d-flex flex-row-reverse flex-lg-row flex-wrap flex-lg-nowrap flex-justify-center flex-lg-justify-between flex-sm-items-center pt-6 pb-2 mt-6 f6 text-gray border-top border-gray-light ">
    <a aria-label="Homepage" title="GitHub" class="footer-octicon d-none d-lg-block mr-lg-4" href="https://kk-karl.github.io/booklog/">
      <svg height="24" class="octicon octicon-mark-github" viewBox="0 0 16 16" version="1.1" width="24">
        <path fill-rule="evenodd"
          d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z">
        </path>
      </svg>
    </a>
    <ul class="list-style-none d-flex flex-wrap col-12 flex-justify-center flex-lg-justify-between mb-2 mb-lg-0">
      
      <li class="mr-3 mr-lg-0">© 2024-2099 mybooklog</li>
      
    </ul>
  </div>
  <div class="d-flex flex-justify-center pb-6">
    <span class="f6 text-gray-light"></span>
  </div>


</div>
</body>

<script type="application/javascript" src="https://kk-karl.github.io/booklog/js/github-style.js"></script>




</html>